# Copyright 2018 Maco
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import time
import json
from tornado import gen
from blockchain.handler.base import FrontendHandler, route
from blockchain.core.blockchain import Bchain
from blockchain.core import proof as sproof
from blockchain.core.nodes import Nodes
from blockchain.core.hash import hash_dict
from blockchain.common import constants
from blockchain.common.util import request
from blockchain.cache.cache import Mcache
from blockchain.core.broadcast import bc_new_transaction, bc_new_block


@route(r'/chain/list')
class ChainListHandler(FrontendHandler):
    '''
    查询所有区块
    :url: /chain/list
    :methods: GET
    :param type: 默认None(返回所有)，可选择返回:chain/length
    '''

    def get(self):
        dtype = self.get_arg('type', None)
        if not dtype or ('chain' in dtype and 'length' in dtype):
            data = {
                'chain': Bchain.chain,
                'length': len(Bchain.chain)
            }
        elif 'chain' in dtype:
            data = {'chain': Bchain.chain}
        else:
            data = {'length': len(Bchain.chain)}
        self.success(data=data)


@route(r'/chain/sync')
class SyncBlockchainHandler(FrontendHandler):
    '''
    同步区块链
    :url: /chain/sync
    :methods: GET
    '''

    @gen.coroutine
    def get(self):
        if len(Nodes.nodes) == 0:
            self.failure('Please add an available node.')
        else:
            # 同步数据...
            result = yield self.get_chain()
            if result:
                self.success('Sync data success.')
            else:
                self.success('No need to sync data.')

    @gen.coroutine
    def get_chain(self):
        '''
        同步数据
        '''
        max_length = 0
        chain_node = None
        for node in Nodes.nodes:
            resp = yield request(f'http://{node}/api/chain/list?type=length')
            if resp and resp.code == constants.CODE_OK:
                data = json.loads(resp.body.decode())
                if data['code'] == constants.CODE_SUCCESS \
                        and data['data']['length'] > max_length:
                    max_length = data['data']['length']
                    chain_node = node
        if chain_node:
            resp = yield request(f'http://{chain_node}/api/chain/list')
            if resp and resp.code == constants.CODE_OK:
                data = json.loads(resp.body.decode())
                if data['code'] == constants.CODE_SUCCESS:
                    Bchain.chain = data['data']['chain']
            raise gen.Return(True)
        raise gen.Return(False)


@route(r'/chain/mine')
class MineHandler(FrontendHandler):
    '''
    挖矿请求，时隔10分钟允许挖矿
    :url: /chain/mine
    '''

    @gen.coroutine
    def get(self):
        last_block = Bchain.last_block
        last_proof = last_block['proof']
        index = last_block['index'] + 1
        diff_time = int(time.time()) - int(last_block['timestamp'])
        if diff_time < constants.MINE_DIFF_TIME:
            seconds = constants.MINE_DIFF_TIME - diff_time
            self.failure(f'Please wait {seconds} seconds and try again.')
        else:
            # 设置开始挖矿
            Mcache.set(constants.MINE_STATUS, 1)
            prev_hash = Bchain.hash(last_block)
            proof = yield self.run_proof(prev_hash, index, Bchain.get_tx_count())
            if proof < 0:
                self.failure('Mining has been terminated and verified.')
            else:
                # Provide rewards for nodes with proof of workload
                # sender="0" indicates newly dug up currency
                Bchain.new_transaction(
                    sender='0', recipient=constants.NODE_UNIQUE, amount=1)
                # Add new blocks to the chain
                block = Bchain.new_block(proof)
                data = {
                    'index': block['index'],
                    'transactions': block['transactions'],
                    'proof': block['proof'],
                    'hash': block['hash'],
                    'prev_hash': block['prev_hash'],
                }
                # 设置结束
                Mcache.set(constants.MINE_STATUS, 0)
                # 广播新块生成
                self.bc_all_nodes(block)
                self.success('New Block Forged.', data)

    async def run_proof(self, prev_hash, index, tx_count):
        data = sproof.proof_of_work(prev_hash, index, tx_count + 1)
        return data

    @gen.coroutine
    def bc_all_nodes(self, block):
        yield bc_new_block(list(Nodes.nodes), block)


@route(r'/chain/proof/vaild')
class VaildProofHandler(FrontendHandler):
    '''
    验证新挖出的块
    :url: /chain/proof/vaild
    :methods: POST
    '''

    @gen.coroutine
    def post(self):
        data = self.get_request_body()
        if not data.get('block', None):
            self.failure('Incorrect data submission.')
        else:
            # 设置挖矿程序暂停
            Mcache.set(constants.MINE_STATUS, 0)
            # 验证block
            block = data['block']
            last_block = Bchain.last_block
            # 是否需要广播到其它节点
            if data.get('_nodes', None):
                self.bc_other_nodes(data['_nodes'], block)

            if last_block['index'] + 1 == block['index']:
                # 工作量证明验证
                vaild = sproof.check_block_proof(block)
                prev_hash = Bchain.hash(last_block)
                if vaild and block['prev_hash'] == prev_hash:
                    Bchain.add_block(block)
                    self.success('Successful verification.')
                else:
                    self.failure('Verification failed.')
            else:
                # sync block
                result = yield Nodes.resolve_conflicts()
                self.success('Successful verification.')

    @gen.coroutine
    def bc_other_nodes(self, sync_nodes, block):
        yield bc_new_block(sync_nodes, block)


@route(r'/transaction/rc/sync')
class RcTransactionHandler(FrontendHandler):
    '''
    同步交易
    :url: /transaction/rc/sync
    :methods: POST
    :param: json e.g:{"ntx":{"sender":"a","recipient":"b,"amount:1,
        "timestamp":1231231},"_nodes":["127.0.0.1:8000"]}
           ntx新交易，_nodes广播的节点列表
    '''

    def post(self):
        data = self.get_request_body()
        if not data.get('ntx', None):
            self.failure('Incorrect data submission.')
        else:
            ntx = data.get('ntx')
            tx_hash = hash_dict(ntx)
            if not tx_hash in Bchain.tx_hash_list:
                index = Bchain.new_transaction(
                    ntx['sender'], ntx['recipient'], ntx['amount'], ntx['timestamp'])
            if data.get('_nodes', None):
                self.bc_other_nodes(data['_nodes'], ntx)
            self.success(f'Transaction will be added to next Block.')

    @gen.coroutine
    def bc_other_nodes(self, sync_nodes, data):
        yield bc_new_transaction(sync_nodes, data)


@route(r'/transaction/new')
class TransactionHandler(FrontendHandler):
    '''
    :url: /transaction/new
    :methods: POST
    :param: json serializable, 
        e.g: {"sender":"a","recipient":"b,"amount:1}
    '''

    @gen.coroutine
    def post(self):
        ntx = self.get_request_body()
        if not ntx:
            self.failure('Incorrect data submission.')
        else:
            required = ['sender', 'recipient', 'amount']
            if not all(key in ntx for key in required):
                self.failure(
                    "Submit data must contain ['sender','recipient','amount']")
            else:
                # Create new transaction.
                index = Bchain.new_transaction(ntx['sender'], ntx[
                                               'recipient'], ntx['amount'])
                self.bc_all_nodes(Bchain.transactions[-1])
                self.success(f'Transaction will be added to Block {index}.')

    @gen.coroutine
    def bc_all_nodes(self, data):
        yield bc_new_transaction(list(Nodes.nodes), data)
